#!/usr/bin/env python
#
# Input manager: provides a mechanism for creating multiple reader instances
# on multiple simultaneous input files (including archives)
#
# Author:   Mike Murphy <mamurph@cs.clemson.edu>
# Revision: 23 September 2009
#
#   Copyright 2009 Clemson University
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# This material is based upon work supported under a National Science
# Foundation Graduate Research Fellowship. Any opinions, findings,
# conclusions or recommendations expressed in this publication are those of
# the author(s) and do not necessarily reflect the views of the National
# Science Foundation.

'''
Flexible input mechanism for reading input files. Input files may be contained
within a tar archive or directory tree and optionally matched by extension.
For simple simulation purposes where no additional reader classes are used
beyond the core classes provided with SimVOC, use of the flex_loader factory
function is recommended.
'''

from common_reader import normalize
from job_format_reader import JobReader
from map_format_reader import MapReader
from vo_format_reader import VOReader
import mimetypes, os, os.path, sys, tarfile

# Constants for use with factory-produced FlexInput instance
JOB = 'job'
MAP = 'map'
VO = 'vo'


class FlexInput(object):
   '''
   Flexible input handler class. This class handles input files in plain
   text formats, nested directory hierarchies containing input files, and
   tar archives (which may contain nested directories) containing input
   files. Actual input file processing is performed by a reader object,
   which instances of this class produce by specifying a named profile for
   each type of reader.
   '''
   def __init__(self, simobj):
      '''
      @param simobj: Reference to the simulator object
      '''
      self.sim = simobj
      self.readers = {}   # name: (class, {kwds})
      self.open_fh = []
      self.open_readers = []
   #
   def add_reader(self, profile, reader_class, **kwds):
      '''
      @param profile: Profile name for the new reader type
      @type profile: str
      @param reader_class: Class (CommonReader child) to instantiate when
                           creating a new reader
      @param kwds: Keyword arguments to pass to the reader class upon
                   instantiation
      '''
      self.readers[profile] = (reader_class, kwds)
   #
   def open_file(self, filename):
      '''
      Opens a single file and adds its handle to the list of open handles

      @param filename: Name of the file to open
      @type filename: str

      @return: open file handle (r mode)
      '''
      print >> sys.stderr, 'Loading file:', filename
      fh = open(filename, 'r')
      self.open_fh.append(fh)
      return fh
   #
   def init_reader(self, filename, fh, profile):
      '''
      Initializes a reader object on the given file, using the specified
      profile. The new reader object is added to the list of open readers
      for later normalization (if desired).

      @param filename: Name of the open file
      @type filename: str
      @param fh: Handle to the open file (r mode)
      @param profile: Name of the reader profile. An Exception will occur if
                           this profile name is not found in the FlexInput
                           instance.
      @type profile: str

      @return: newly created reader object
      '''
      cls, kwds = self.readers[profile]
      r = cls(filename, fh, self.sim, **kwds)
      self.open_readers.append(r)
      return r
   #
   def load(self, path, profile, extension=None):
      '''
      Loads a single text file or a set of several text files contained in a
      directory or tar archive (using an optional matching extension).

      @param path: Path to open for input. May be a single text file, a tar
                   archive, or a directory tree containing text files.
      @type path: str
      @param profile: Profile name used to select which reader class will be
                      instantiated
      @type profile: str
      @param extension: File extension for filename testing (uses endswith,
                       case-sensitive) when the path references a tarfile
                       or directory. Only files matching the extension will
                       be loaded. If extension is None, then all files in the
                       given path will be loaded. This option has no effect
                       if path references a single text file.
      @type extension: str

      @return: list of reader objects
      '''
      r = []
      if os.path.isdir(path):
         for root, dirs, files in os.walk(path):
            for name in files:
               if (extension is None) or (name.endswith(extension)):
                  filename = os.path.join(root, name)
                  fh = self.open_file(filename)
                  r.append(self.init_reader(filename, fh, profile))
      else:
         t, c = mimetypes.guess_type(path)
         if t == 'application/x-tar':
            print >> sys.stderr, 'Loading archive:', path
            tfh = tarfile.open(path, 'r')
            self.open_fh.append(tfh)   # append tfh before member fh records,
                                       # since close behaves like a stack
            for m in tfh.getmembers():
               if (extension is None) or (m.name.endswith(extension)):
                  print >> sys.stderr, '-- Loading archive member:', m.name
                  fh = tfh.extractfile(m)
                  self.open_fh.append(fh)
                  filename = path + '::' + m.name
                  r.append(self.init_reader(filename, fh, profile))
            #
         else:
            fh = self.open_file(path)
            r.append(self.init_reader(path, fh, profile))
         #
      #
      return r
   #
   def close(self):
      '''
      Closes all open file handles. Errors will occur if this method is called
      before the simulation ends.
      '''
      while self.open_fh != []:
         fh = self.open_fh.pop()
         fh.close()
   #
   def normalize(self):
      '''
      Performs a simulation-time normalization of all open readers. No
      additional readers should be opened after the normalization is
      completed, and normalization should occur before starting the
      simulator.
      '''
      normalize(self.open_readers)
   #
#


def flex_loader(simobj, grid, vo_map, jobmodfn=None):
   '''
   Factory function that creates a FlexInput instance for the reader types
   included with the SimVOC core package (job, map, and VO). Profiles for
   these types will be named using the constants provided by the containing
   module: JOB, MAP, VO.

   @param simobj: Reference to the simulator object
   @param grid: Reference to the simulated grid
   @param vo_map: VO map (dictionary-like object) used by the simulation
   @param jobmodfn: Optional job modification function (see the
                    L{job_format_reader} documentation)

   @return: newly created FlexInput instance
   '''
   fi = FlexInput(simobj)
   fi.add_reader(JOB, JobReader, grid=grid, modfn=jobmodfn)
   fi.add_reader(MAP, MapReader, grid=grid)
   fi.add_reader(VO, VOReader, vo_map=vo_map)
   return fi
#
